/**
 * @fileoverview Universal Serializer for QuickJS Environment.
 * Converts arbitrary JavaScript values into a JSON-compatible Intermediate Representation (IR),
 * handling complex types (Date, RegExp, Map, Set, BigInt, Symbol keys, TypedArrays, etc.)
 * and circular references.
 *
 * IMPORTANT: This code is intended to run *inside* QuickJS. It must be self-contained
 * and use only JavaScript features available in the target QuickJS version (ES2020+).
 * It CANNOT use browser/Node APIs or external libraries like lodash.
 */

// --- START: Embedded Base64 Helpers (No 'btoa' in QuickJS) ---
// Basic Base64 encoding suitable for environments without btoa
// Based on https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
const base64Encode = (function() {
    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=';

    function encode(inputBytes) {
        let base64 = '';
        let i = 0;
        const len = inputBytes.length;

        while (i < len) {
            const byte1 = inputBytes[i++];
            const byte2 = i < len ? inputBytes[i++] : NaN;
            const byte3 = i < len ? inputBytes[i++] : NaN;

            const enc1 = byte1 >> 2;
            const enc2 = ((byte1 & 3) << 4) | (byte2 >> 4);
            const enc3 = ((byte2 & 15) << 2) | (byte3 >> 6);
            const enc4 = byte3 & 63;

            base64 += chars.charAt(enc1);
            base64 += chars.charAt(enc2);
            base64 += isNaN(byte2) ? '=' : chars.charAt(enc3);
            base64 += isNaN(byte3) ? '=' : chars.charAt(enc4);
        }
        return base64;
    }

    return {
        bytesToBase64: function(bytes) {
            if (!(bytes instanceof Uint8Array)) {
                // If it's an ArrayBuffer, create a Uint8Array view
                if (bytes instanceof ArrayBuffer) {
                    bytes = new Uint8Array(bytes);
                } else {
                    // Attempt to handle plain arrays of numbers (0-255)
                    try {
                         bytes = Uint8Array.from(bytes);
                    } catch (e) {
                        throw new Error("Input must be Uint8Array, ArrayBuffer, or an array of bytes (0-255).");
                    }
                }
            }
            return encode(bytes);
        }
    };
})();
// --- END: Embedded Base64 Helpers ---

/**
 * Represents the state during serialization, tracking visited objects/symbols
 * for cycle detection and assigning unique reference IDs.
 */
function createSerializationState() {
    return {
        /** Map from original object/symbol/function to its assigned reference ID. */
        visitedToReferenceId: new Map(),
        /** Counter for assigning unique reference IDs. */
        nextReferenceId: 1,
    };
}

/**
 * Main serialization function.
 * Converts a JavaScript value into the Universal Intermediate Representation (IR).
 *
 * @param {*} data The JavaScript value to serialize.
 * @param {object} [state] Internal state for tracking cycles. Do not provide manually.
 * @returns {object|string|number|boolean|null} The serialized IR.
 */
function serializeToUniversalIR(data, state) {
    // Initialize state for the top-level call
    state = state || createSerializationState();

    const type = typeof data;

    // 1. Handle primitives directly supported by JSON (or Null)
    if (data === null) {
        // Optionally return a tagged Null, but null itself is JSON-safe
        // return { __universalDataType__: 'Null' };
        return null;
    }
    if (type === 'string' || type === 'number' || type === 'boolean') {
        // Primitives don't need IDs unless they *could* be part of a cycle
        // which isn't possible for immutable primitives. Return directly.
        return data;
    }

    // 2. Handle Undefined
    if (type === 'undefined') {
        return { __universalDataType__: 'Undefined' };
    }

    // 3. Handle BigInt
    if (type === 'bigint') {
        return {
            __universalDataType__: 'BigInt',
            value: data.toString(),
            // BigInts are immutable, no refId needed currently
        };
    }

    // 4. Handle Symbol
    if (type === 'symbol') {
        // Symbols *can* be targets of references if reused, so assign ID
        if (state.visitedToReferenceId.has(data)) {
            return {
                __universalDataType__: 'CircularRef',
                __universalRefId__: state.visitedToReferenceId.get(data),
            };
        }
        const refId = state.nextReferenceId++;
        state.visitedToReferenceId.set(data, refId);
        return {
            __universalDataType__: 'Symbol',
            description: data.description,
            __universalRefId__: refId,
        };
    }

    // --- From here, we are dealing with Objects (including arrays, functions, etc.) --- //
    // 5. Handle Circular References for Objects/Functions
    if (state.visitedToReferenceId.has(data)) {
        return {
            __universalDataType__: 'CircularRef',
            __universalRefId__: state.visitedToReferenceId.get(data),
        };
    }

    // --- Assign Reference ID and mark as visited --- //
    const currentRefId = state.nextReferenceId++;
    state.visitedToReferenceId.set(data, currentRefId);

    // 6. Handle specific object types
    if (data instanceof Date) {
        return {
            __universalDataType__: 'Date',
            __universalRefId__: currentRefId,
            value: data.toISOString(),
        };
    }

    if (data instanceof RegExp) {
        return {
            __universalDataType__: 'RegExp',
            __universalRefId__: currentRefId,
            source: data.source,
            flags: data.flags,
        };
    }

    if (data instanceof Error) {
        let causeIR = undefined;
        if ('cause' in data && data.cause !== undefined) {
            // Recursively serialize the cause
            causeIR = serializeToUniversalIR(data.cause, state);
        }
        return {
            __universalDataType__: 'Error',
            __universalRefId__: currentRefId,
            name: data.name,
            message: data.message,
            stack: data.stack, // Stack trace might differ across engines
            cause: causeIR,
        };
    }

    // Handle Binary Data: ArrayBuffer and TypedArrays
    if (data instanceof ArrayBuffer) {
        try {
            return {
                __universalDataType__: 'ArrayBuffer',
                __universalRefId__: currentRefId,
                base64: base64Encode.bytesToBase64(data),
            };
        } catch (e) {
            console.warn('[Serializer] Failed to encode ArrayBuffer:', e);
            return {
                __universalDataType__: 'Error',
                __universalRefId__: currentRefId,
                name: 'SerializationError',
                message: 'Failed to encode ArrayBuffer to Base64',
            };
        }
    }
    if (ArrayBuffer.isView(data) && !(data instanceof DataView)) {
        // Check if it's a TypedArray (Uint8Array, Float32Array, etc.)
        try {
            return {
                __universalDataType__: 'TypedArray',
                __universalRefId__: currentRefId,
                constructorName: data.constructor.name,
                base64: base64Encode.bytesToBase64(data.buffer.slice(data.byteOffset, data.byteOffset + data.byteLength)),
            };
        } catch (e) {
             console.warn('[Serializer] Failed to encode TypedArray:', e);
            return {
                __universalDataType__: 'Error',
                __universalRefId__: currentRefId,
                name: 'SerializationError',
                message: 'Failed to encode TypedArray to Base64',
            };
        }
    }

    // Handle Collections: Map, Set, WeakMap, WeakSet
    if (data instanceof Map) {
        const serializedEntries = [];
        // Must iterate and serialize keys/values recursively
        for (const [key, value] of data.entries()) {
            serializedEntries.push([
                serializeToUniversalIR(key, state), // Serialize key
                serializeToUniversalIR(value, state), // Serialize value
            ]);
        }
        return {
            __universalDataType__: 'Map',
            __universalRefId__: currentRefId,
            entries: serializedEntries,
        };
    }

    if (data instanceof Set) {
        const serializedValues = [];
        for (const value of data.values()) {
            serializedValues.push(serializeToUniversalIR(value, state));
        }
        return {
            __universalDataType__: 'Set',
            __universalRefId__: currentRefId,
            values: serializedValues,
        };
    }

    if (data instanceof WeakMap) {
        return {
            __universalDataType__: 'WeakMap',
            __universalRefId__: currentRefId,
            // No content serialized
        };
    }

    if (data instanceof WeakSet) {
        return {
            __universalDataType__: 'WeakSet',
            __universalRefId__: currentRefId,
            // No content serialized
        };
    }

    // Handle Arrays
    if (Array.isArray(data)) {
        const serializedValues = data.map(function(item) {
            return serializeToUniversalIR(item, state);
        });
        return {
            __universalDataType__: 'Array',
            __universalRefId__: currentRefId,
            values: serializedValues,
        };
    }

    // Handle Functions
    if (type === 'function') {
        let source = '[Function source unavailable]';
        try {
            source = data.toString();
        } catch (e) { /* Ignore errors getting source */ }
        return {
            __universalDataType__: 'Function',
            __universalRefId__: currentRefId,
            source: source,
        };
    }

    // Handle Plain Objects (or treat other complex objects similarly)
    if (type === 'object') {
        const serializedEntries = [];
        // Use Reflect.ownKeys to get both string and symbol keys
        const keys = Reflect.ownKeys(data);

        for (let i = 0; i < keys.length; i++) {
            const key = keys[i];
            const value = data[key];
            let keyIR;

            // Serialize the key itself (might be string or symbol)
            if (typeof key === 'string') {
                keyIR = key;
            } else if (typeof key === 'symbol') {
                keyIR = serializeToUniversalIR(key, state); // Serialize symbol key
            } else {
                console.warn('[Serializer] Encountered unexpected key type:', typeof key, key);
                // Skip this key or represent it somehow?
                continue;
            }

            // Serialize the value
            const valueIR = serializeToUniversalIR(value, state);

            serializedEntries.push([keyIR, valueIR]);
        }

        return {
            __universalDataType__: 'Object',
            __universalRefId__: currentRefId,
            // Optional: Add className: data.constructor?.name if needed
            entries: serializedEntries,
        };
    }

    // Fallback for unrecognized types (should be rare)
    console.warn('[Serializer] Unhandled data type:', type, data);
    let fallbackValue = '[Unserializable Data]';
    try {
        fallbackValue = String(data);
    } catch (e) { /* Ignore */ }
    return {
        __universalDataType__: 'Unknown',
        __universalRefId__: currentRefId,
        value: fallbackValue,
    };
}

// If this script were loaded as a module, we'd export:
// export { serializeToUniversalIR };
// Since it's likely injected as a string, the function becomes globally available
// within the QuickJS context where the wrapper script is evaluated. 